package eu.goldenak.ircbot;

import eu.goldenak.ircbot.BotGroup.DistributeMessage;
import eu.goldenak.ircbot.module.*;
import java.io.File;
import java.lang.reflect.Method;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.Hashtable;
import java.util.Set;

/**
 * The ModuleManager class does exactly what the name suggests, it manages the 
 * bot's modules. Since there's only one instance of every module, we need to 
 * make sure there is only one instance of the ModuleManager as well. We insure
 * this by utilizing the Singleton pattern. Extra advantage of this is that any
 * module can call the ModuleManager and use its functionality to extend the 
 * capabilities of the module.
 */
public class ModuleManager extends Thread implements ModuleCallbacks {
	/**
	 * The only instance to this object.
	 */
	private static final ModuleManager m_pInstance = new ModuleManager ();
	
	/**
	 * Hashtable with the Module name as the key and the actual
	 * ModuleClassLoader as value.
	 */
	private Hashtable <String, ModuleClassLoader> m_htModules = 
		new Hashtable <String, ModuleClassLoader> ();
	
	/**
	 * Whether to process the modules.
	 */
	private boolean m_bRun = true;
	
	/**
	 * We make the constructor private in order to prevent unwanted 
	 * initializations.
	 */
	private ModuleManager () {}
	
	/**
	 * Returns the instance to this ModuleManager. Since we want the modules to
	 * be loaded only once for each one, we don't want any more than one 
	 * instance of the ModuleManager either.
	 */
	public static synchronized ModuleManager getInstance () {
		return m_pInstance;
	}
	
	/**
	 * Loads up one module using the ModuleClassLoader. Returns true when the 
	 * Module has been successfully loaded, false otherwise. An error message
	 * will be sent to System.err.
	 */
	public boolean loadModule (String sName) {
		ModuleClassLoader pModuleLoader;
		
		try {
			pModuleLoader = new ModuleClassLoader (sName);
		} catch (ClassNotFoundException pException) {
			System.err.println ("Module '" + sName + "' not found, typing " +
				"error in configfile?");
			System.err.println (pException);
			
			return false;
		}
		
		/*if (!clModule.getSuperclass ().getSimpleName ().equals ("ModuleBase")) {
			System.err.println ("The module '" + sName + "' does not " +
				"implement ModuleBase.");
			
			return false;
		}*/
		
		if (new File ("config", sName + ".cfg").exists ()) {
			// Load the configfile for this module.
			try {
				ConfigManager.loadConfigFile (sName);
			} catch (Exception pException) {
				System.err.println ("Loading configfile for '" + sName +
					"' failed: " + pException.getMessage ());
				
				return false;
			}
		}
		
		try {
			pModuleLoader.init ();
		} catch (Exception pException) {
			System.err.println ("Module '" + sName + "' could not be " +
				"instantiated: " + pException.getMessage ());
			
			return false;
		}
		
		m_htModules.put (sName, pModuleLoader);
		
		return true;
	}
	
	/**
	 * Loads up a couple of modules from a String array.
	 */
	public void loadModules (String [] aModuleNames) {
		for (String sModuleName : aModuleNames) {
			loadModule (sModuleName);
		}
	}
	
	/**
	 * A simple shortcut for unloadModule() and loadModule().
	 */
	public boolean reloadModule (String sName) {
		unloadModule (sName);
		return loadModule (sName);
	}
	
	/**
	 * This method will get rid of the specified module. This includes the class
	 * in the memory. After this method has been executed, loadModule() can be
	 * used again to reload the same Module from the .class file.
	 */
	public void unloadModule (String sName) {
		m_htModules.remove (sName);
	}
	
	/**
	 * Returns true if the given name is a name of a loaded Module. If not, then
	 * false is returned, as expected.
	 */
	public boolean isModuleLoaded (String sName) {
		return m_htModules.containsKey (sName);
	}
	
	/**
	 * Returns whether the modules are currently being processed.
	 */
	public boolean isEnabled () {
		return m_bRun;
	}
	
	/**
	 * Enables processing of the modules.
	 */
	public void enable () {
		m_bRun = true;
	}
	
	/**
	 * Disables processing of the modules.
	 */
	public void disable () {
		m_bRun = false;
	}
	
	/**
	 * The thread which handles the modules.
	 */
	public void run () {
		// No Exceptions here, since loading Modules is non-critical.
		loadModules (ConfigManager.get ("Main", "modules").split (","));
		
		// disable() will be called by the BotManager when it's time to clean
		// up and shut down.
		while (isEnabled ()) {
			loop ();
			checkDistributeMessages ();
			
			// Don't stress the CPU too much.
			try {
				Thread.sleep (50);
			} catch (InterruptedException pException) {}
		}
	}
	
	/**
	 * Loop once through all modules.
	 */
	private void loop () {
		Set <String> pModuleNames = m_htModules.keySet ();
		Iterator <String> itModuleNames = pModuleNames.iterator ();
		
		while (itModuleNames.hasNext ()) {
			m_htModules.get (itModuleNames.next ()).getModule ().onTick ();
		}
	}
	
	/**
	 * This method asks every module if there's messages that need to be
	 * distributed. TODO: This is going to go in the near future.
	 */
	private void checkDistributeMessages () {
		Set <String> pModuleNames = m_htModules.keySet ();
		Iterator <String> itModuleNames = pModuleNames.iterator ();
		
		while (itModuleNames.hasNext ()) {
			String sModuleName = itModuleNames.next ();
			DistributeMessage [] aMessages =
				m_htModules.get (sModuleName).getModule ().getDistributeMessages ();
			
			if (aMessages == null) continue;
			
			BotGroup pBots = BotManager.getInstance ().getBots ();
			
			for (DistributeMessage pMessage : aMessages) {
				pBots.privmsg (pMessage);
			}
		}
	}
	
	/**
	 * This method can be used to get a pointer to a specified module. For
	 * whatever you might need it. Throws an Exception when the specified
	 * modulename was not found.
	 */
	public ModuleBase getModule (String sModuleName) throws Exception {
		if (m_htModules.containsKey (sModuleName)) {
			return m_htModules.get (sModuleName).getModule ();
		}
		
		throw new Exception ("Module '" + sModuleName + "' not found. Either " +
			"not loaded or non-existant.");
	}
	
	/**
	 * This method will call the specified method on all loaded modules.
	 */
	private void call (String sMethodName, Object ... aArguments) {
		Class [] aClasses = new Class [aArguments.length];
		
		for (int i = 0; i < aArguments.length; i ++) {
			aClasses [i] = aArguments [i].getClass ();
		}
		
		Set <String> pModuleNames = m_htModules.keySet ();
		Iterator <String> itModuleNames = pModuleNames.iterator ();
		
		while (itModuleNames.hasNext ()) {
			ModuleBase pModule;
			
			try {
				pModule = m_htModules.get (itModuleNames.next ()).getModule ();
			} catch (ConcurrentModificationException pException) {
				break;
			}
			
			try {
				Method pMethod = pModule.getClass ().getMethod (sMethodName,
					aClasses);
				
				pMethod.invoke (pModule, aArguments);
			} catch (Exception pException) {
				System.out.println (pException.getMessage ());
				pException.printStackTrace ();
			}
		}
	}
	
	/**
	 * Someone changed his/her nickname. Note that this event doesn't supply
	 * the channel(s) it occurred in.
	 */
	public void onChangeNick (IrcBot pBot, String sOldNick, String sNewNick) {
		call ("onChangeNick", pBot, sOldNick, sNewNick);
	}
	
	/**
	 * The topic of a channel has been changed.
	 */
	public void onChangeTopic (IrcBot pBot, String sChannel, String sNickname, String sNewTopic) {
		call ("onChangeTopic", pBot, sChannel, sNickname, sNewTopic);
	}
	
	/**
	 * When joining a channel, you get sent a bit of info, including the topic.
	 * That gets handled in here.
	 */
	public void onChannelTopic (IrcBot pBot, String sChannel, String sTopic) {
		call ("onChannelTopic", pBot, sChannel, sTopic);
	}
	
	/**
	 * When joining a channel, you also get a list with the names who are
	 * currently in the channel. Note that prefixes are in the names (~&@%+).
	 */
	public void onChannelNames (IrcBot pBot, String sChannel, String sNames) {
		call ("onChannelNames", pBot, sChannel, sNames);
	}
	
	/**
	 * Someone joined a channel.
	 */
	public void onChannelJoin (IrcBot pBot, String sChannel, String sNickname) {
		call ("onChannelJoin", pBot, sChannel, sNickname);
	}
	
	/**
	 * Someone was kicked from a channel by someone.
	 */
	public void onChannelKick (IrcBot pBot, String sChannel, String sKicked, String sKicker, String sReason) {
		call ("onChannelKick", pBot, sChannel, sKicked, sKicker, sReason);
	}
	
	/**
	 * A mode has been set on a channel or on the bot itself.
	 */
	public void onChannelMode (IrcBot pBot, String sChannel, String sMode) {
		call ("onChannelMode", pBot, sChannel, sMode);
	}
	
	/**
	 * Someone left the channel.
	 */
	public void onChannelPart (IrcBot pBot, String sChannel, String sNickname, String sMessage) {
		call ("onChannelPart", pBot, sChannel, sNickname, sMessage);
	}
	
	/**
	 * A PRIVMSG event in a channel. For personal PRIVMSGs see onPrivmsg().
	 */
	public void onChannelPrivmsg (IrcBot pBot, String sChannel, String sNickname, String sMessage) {
		call ("onChannelPrivmsg", pBot, sChannel, sNickname, sMessage);
	}
	
	/**
	 * This method gets called as soon as we are connected and logged in to the 
	 * IRC server.
	 */
	public void onConnect (IrcBot pBot) {
		call ("onConnect", pBot);
	}
	
	/**
	 * This method gets called when someone sends us a client-to-client protocol
	 * message (prefixed and suffixed by a 0x01 char).
	 */
	public void onCTCP (IrcBot pBot, String sChannel, String sNickname, String sType, String sMessage) {
		call ("onCTCP", pBot, sChannel, sNickname, sType, sMessage);
	}
	
	/**
	 * As soon as we get an invitation to join a channel this method gets 
	 * called.
	 */
	public void onInvite (IrcBot pBot, String sInviter, String sInvitee, String sChannel) {
		call ("onInvite", pBot, sInviter, sInvitee, sChannel);
	}
	
	/**
	 * When we receive a notice, this callback will be executed.
	 */
	public void onNotice (IrcBot pBot, String sChannel, String sNickname, String sMessage) {
		call ("onNotice", pBot, sChannel, sNickname, sMessage);
	}
	
	/**
	 * A personal PRIVMSG.
	 */
	public void onPrivmsg (IrcBot pBot, String sNickname, String sMessage) {
		call ("onPrivmsg", pBot, sNickname, sMessage);
	}
	
	/**
	 * Someone quit from the IRC server. Note that this event doesn't have a 
	 * channel in it.
	 */
	public void onQuit (IrcBot pBot, String sNickname, String sMessage) {
		call ("onQuit", pBot, sNickname, sMessage);
	}
	
	/**
	 * This method will get called everytime a message is received. If you're 
	 * considering on using this, you're probably looking for 
	 * onUnhandledCommand().
	 */
	public void onRaw (IrcBot pBot, String sRaw) {
		call ("onRaw", pBot, sRaw);
	}
	
	/**
	 * This method will be called after a message has been sent to the IRC
	 * server.
	 */
	public void onRawSend (IrcBot pBot, String sRaw) {
		call ("onRawSend", pBot, sRaw);
	}
	
	/**
	 * This method will be called when the bot shuts down. This because of the
	 * lack of destructors in Java.
	 */
	public void onShutdown (IrcBot pBot) {
		call ("onShutdown", pBot);
	}
	
	/**
	 * This method will be called with every loop the main loop makes. Use this
	 * for polling stuff or something.
	 */
	public void onTick () {
		call ("onTick");
	}
	
	/**
	 * This method will be called whenever a message with an unknown IRC event
	 * has been received. Use this when is no callback for the type of message
	 * you want to process.
	 */
	public void onUnhandledCommand (IrcBot pBot) {
		call ("onUnhandledCommand", pBot);
	}
}